10. Warsztaty JS cz.1
Wyzwania:
- przećwiczysz po raz kolejny podstawy JS-a,
- nauczysz się dzielić kod JS na komponenty,
- poznasz dziedziczenie klas w OOP,
- stworzysz wyjątkowy formularz rezerwacji stolików.
Wstęp
Za nami już naprawdę dużo pracy. Odkąd zaczęliśmy pracować z JS-em, tempo kursu gwałtownie przyśpieszyło. Warto więc na chwilę się zatrzymać i trochę potrenować. Przed Tobą dwa moduły warsztatów, w których bardzo mocno przećwiczymy to, co już znasz, ale również poznamy kolejne nowe zagadnienia i przede wszystkim – sprawdzimy Cię w bojach.
W poprzednich modułach staraliśmy się bardzo pomagać, tym razem w dużej mierze przeniesiemy ciężar pisania kodu na Ciebie. Nie obawiaj sie jednak. Zaczniemy od rozgrzewki, która wcale nie będzie aż taka trudna. Potem z kolei zawsze będziemy mimo wszystko wspierać Cię malutkimi wskazówkami. Nie będzie aż tak źle! ;)
10.1. Rozgrzewka
Zaczniemy od naprawdę krótkich ćwiczeń, powoli przechodząc do coraz trudniejszych. Przygotuj sobie otwarte okno Codepena lub edytor, przydadzą się do wykonywania kolejnych wyzwań. Pod każdym zadaniem ukryte jest rozwiązanie, które pozwoli Ci sprawdzić, jak można było je wykonać. Nie otwieraj go jednak tak długo, jak Twoja wersja nie będzie gotowa.
Pierwsze kroki – tablice i obiekty
Zaczniemy od krótkich ćwiczeń. Nie będą one bardzo trudne, ale jednak powinny stanowić dla Ciebie małe wyzwanie. Postaraj się podejść do nich "na logikę", zastanowić się, co komputer musiałby zrobić, jakie kroki podjąć, aby dojść do konkretnego efektu.
Ćwiczenie 1
Istnieje podana tablica:
const names = ['Kasia', 'Tomek', 'Amanda', 'Maja'];
Twoim zadaniem jest stworzenie nowej, w której będą zawarte tylko imiona żeńskie obecne w tej oryginalnej. Dla uproszczenia załóżmy, że imiona żeńskie to takie, które kończą się na a.
Do wykonania ćwiczenia przydatne mogą być wbudowane w JS-a metody. Zainteresuj się zwłaszcza metodami slice lub charAt. Być może pomocna będzie również informacja, że właściwość length użyta na stringu, zwraca jego długość.
Pokaż podpowiedź Ukryj podpowiedź
Zastanów się, jak powinien wyglądać nasz algorytm.
- JS na pewno musi zacząć od przygotowania nowej tablicy, na starcie pustej, do której sukcesywnie będzie dodawać kolejne elementy.
- Następnie musi przejść po każdym elemencie tablicy z imionami i dla każdego elementu sprawdzić, jaką wartość ma jego ostatnia litera.
- Jeśli dane imię kończy się na a, to należy dodać je do tej nowej tablicy.
- Tym samym na końcu będzie ona tablicą przefiltrowanych wyników.
Pokaż rozwiązanie Ukryj rozwiązanie
const names = ['Kasia', 'Tomek', 'Amanda', 'Maja'];
const filteredNames = [];
for(const name of names) {
if(name.slice(-1) === 'a') {
filteredNames.push(name);
}
}
Ćwiczenie 2
const employees = {
john: {
name: 'John Doe',
salary: 3000
},
amanda: {
name: 'Amanda Doe',
salary: 4000
},
}
To już trochę trudniejsze zadanie. Za pomocą pętli for przejdź po każdym obiekcie w employees i wygeneruj dwie nowe tablice. employeesNames powinno być listą imion pracowników (tylko imion!). employeesSalaries powinno być listą pensji.
Uwaga! Bardzo przydatna może okazać się znana Ci już metoda split.
Pokaż rozwiązanie Ukryj rozwiązanie
const employees = {
john: {
name: 'John Doe',
salary: 3000
},
amanda: {
name: 'Amanda Doe',
salary: 4000
},
}
const employeesNames = [];
const employeesSalaries = [];
for(const employeeId in employees) {
const employee = employees[employeeId];
// split name at ' ' and get first element
// (John Doe -> ['John', 'Doe'] -> 'John')
const name = employee.name.split(' ')[0];
employeesNames.push(name);
employeesSalaries.push(employee.salary);
}
Ćwiczenie 3
const salaries = [2000, 3000, 1500, 6000, 3000];
Twoim zadaniem jest ustalenie i wyświetlenie w konsoli:
- sumy wszystkich pensji
- najwyższej pensji
- najniższej pensji
Pokaż wskazówkę Ukryj wskazówkę
Obliczenie sumy zapewne nie będzie dla Ciebie problemem, ale jak poradzić sobie z największą i najmniejszą wartością?
Jak ustalić największą wartość? Najlepiej również wspomóc się pętlą. Jeśli stworzysz jakąś zmienną i nadasz jej wartość pierwszego elementu tablicy, to następnie wystarczy porównywać jej wartość do kolejnych sprawdzanych pensji. Jeśli kolejna pensja jest większa od tej, która aktualnie w tej zmiennej jest, to należy podmienić wartość zmiennej. Tym samym na końcu działania pętli, wartość która w tej zmiennej pozostanie, będzie można uznać za największą.
Podobnie postąpimy przy szukaniu najmniejszej pensji.
Pokaż rozwiązanie Ukryj rozwiązanie
const salaries = [2000, 3000, 1500, 6000, 3000];
let sum = 0;
let highestSalary = salaries[0];
let lowestSalary = salaries[0];
for(const salary of salaries) {
sum += salary;
if(salary > highestSalary) highestSalary = salary;
if(salary < lowestSalary) lowestSalary = salary;
}
console.log(sum, highestSalary, lowestSalary);
Ćwiczenie 4
const persons = {
john: 2000,
amanda: 3000,
thomas: 1500,
james: 6000,
claire: 3000
};
Dla praktyki, mamy dla Ciebie podobny przykład, tylko że tym razem dane wejściowe to obiekt, a nie tablica.
Znowu Twoim zadaniem jest ustalenie i wyświetlenie w konsoli:
- sumy wszystkich pensji
- najwyższej pensji
- najniższej pensji
Wskazówka: Możesz zastanowić się nad wspomożeniem się wbudowaną metodą Object.values. Nie jest to konieczne, ale jest to zdecydowanie krótsza opcja.
Pokaż rozwiązanie Ukryj rozwiązanie
const persons = {
john: 2000,
amanda: 3000,
thomas: 1500,
james: 6000,
claire: 3000
}
let sum = 0;
let highestSalary = persons.john;
let lowestSalary = persons.john;
for(const personId in persons) {
const personSalary = persons[personId];
sum += personSalary;
if(personSalary > highestSalary) highestSalary = personSalary;
if(personSalary < lowestSalary) lowestSalary = personSalary;
}
console.log(sum, highestSalary, lowestSalary);
lub
const persons = {
john: 2000,
amanda: 3000,
thomas: 1500,
james: 6000,
claire: 3000
}
// covert obj to array of its values ([2000, 3000, 1500...])
const salaries = Object.values(persons);
let sum = 0;
let highestSalary = salaries[0];
let lowestSalary = salaries[0];
for(const salary of salaries) {
sum += salary;
if(salary > highestSalary) highestSalary = salary;
if(salary < lowestSalary) lowestSalary = salary;
}
console.log(sum, highestSalary, lowestSalary);
Ćwiczenie 5
Czas na ostatni przykład z pierwszego etapu.
const tags = ['news', 'code', 'news', 'sport', 'hot', 'news', 'code'];
Twoim zdaniem jest zbudowanie na podstawie powyższej tablicy, obiektu uniqueTags, który posiada tylko unikalne tagi. W taki sposób, że każdy unikalny tag, to nowa właściwość w tym obiekcie. Jego wartością powinien być za to kolejny obiekt z właściwością appearances o wartości liczby wystąpień tego tagu.
Czyli, dla naszych tagów wyżej, powinno wygenerować się coś w stylu:
{
news: { appearances: 3 },
code: { appearances: 2 },
sport: { appearances: 1 },
hot: { appearances: 1 },
}
Pokaż wskazówkę Ukryj wskazówkę
Zastanów się nad algorytmem. JS na pewno musi posiadać nowy pusty obiekt, który następnie będzie rozwijany o kolejne właściwości. Na pewno musimy przejść po wszystkich elementach z tablicy tags i każdy sprawdzić. Należy ustalić, czy dany tag był już w tym obiekcie podsumowania, czy nie. Jeśli nie, to należy dodać właściwość o nazwie równej nazwie tagu oraz o wartości równej { appearances: 1 }. Jeśli już taki tag w tym obiekcie podsumowania jest, to należy tylko zwiększyć jego właściwość appearances o 1.
Pokaż rozwiązanie Ukryj rozwiązanie
const tags = ['news', 'code', 'news', 'sport', 'hot', 'news', 'code'];
const uniqueTags = {};
for(const tag of tags) {
if(!uniqueTags[tag]) uniqueTags[tag] = { appearances: 1 };
else uniqueTags[tag].appearances++;
}
Praca z funkcjami
Przejdziemy teraz krok dalej. Zabierzemy się za funkcje. Zaczniemy jednak od pytań, w których nie będzie koniecznie pisanie ani linijki kodu. Dopiero później przejdziemy do bardziej praktycznych przykładów.
Ćwiczenie 1
const foo = 4;
function Bar() {
const foo = 5;
const spam = 6;
console.log(foo, spam, eggs);
function Baz() {
const spam = 7;
const eggs = 8;
console.log(foo, spam);
}
}
Mamy dla Ciebie kilka pytań:
- Czy funkcja
Bazmoże mieć dostęp do stałychfooispamz zakresu funkcjiBar? - Czy funkcja
Barma dostęp do stałejeggsz zakresuBaz? - Czy to, że stała o takiej samej nazwie (
foo) jest deklarowana w dwóch zakresach (zakres globalny i zakresBar) spowoduje jakiś błąd? - Gdybyśmy uruchomili funkcję
Baz, to jaką wartość pokaże nam jakofoo?
Pokaż odpowiedź Ukryj odpowiedź
- Tak. Zakres wewnętrzny zawsze ma dostęp do zakresu zewnętrznego.
- Nie. Zakres zewnętrzny nie ma dostępu do zakresu wewnętrznego.
- Nie. Każdy zakres może mieć swoją własną stałą o danej nazwie. Nie możemy więc powtórzyć nazwy
foow jednym zakresie, ale kolejny jak najbardziej może mieć swoją własną stałą o nazwiefoo. - Wartość wziętą z zakresu wyżej, a więc
5. JS zawsze szuka danej zmiennej/stałej najpierw w zakresie samej funkcji, ale jeśli się mu nie powiedzie, to przejdzie wyżej, do zakresu zewnętrznego. Jeśli i tam takiej informacji nie znajdzie, to pójdzie jeszcze wyżej... aż do zakresu globalnego.
Ćwiczenie 2
O "zakresie" mówimy tylko w przypadku funkcji?
const foo = 4;
function Bar(param) {
const baz = 5;
if(param === 1) {
const spam = 6;
}
}
Pokaż odpowiedź Ukryj odpowiedź
Nie. Istnieje również zakres globalny. Dodatkowo każde {} także tworzy nowy zakres. Każdy if, każda pętla for itd. tym samym oznacza również utworzenie nowego zakresu.
W naszym przykładzie będziemy więc posiadać trzy zakresy:
- zakres globalny,
- zakres funkcji Bar,
- zakres pętli if.
Ćwiczenie 3
Czy słowo kluczowe this zależy od miejsca wywołania funkcji? Jakie wartości this pojawią się w konsoli, po uruchomieniu poniższego kodu?
'use strict';
const foo = function() {
console.log(this);
}
const obj = {
foo: foo
}
foo();
foo.call(5);
obj.foo();
Pokaż odpowiedź Ukryj odpowiedź
Tak, co można zresztą obserwować na powyższym przykładzie.
Za każdy razem włączamy jedną i tę samą funkcję. Zauważ, że obj.foo to też tylko referencja (adres) do funkcji foo zadeklarowanej wyżej. Trzy razy wywołujemy więc jedną i tę samą funkcję. Za pierwszym razem pokaże ona w konsoli undefined (pierwsza zasada – default rule), za drugim razem 5 (call "wymusza" podane this), a za trzecim obj (druga zasada – funkcja uruchomiona na obiekcie, wskazuje właśnie na ten obiekt).
Ćwiczenie 4
Czas na przejście do bardziej praktycznych zadań.
Napisz funkcję filterEmployees, która będzie pobierać dwa argumenty:
- tablicę z obiektami o strukturze
{ name: 'Imię nazwisko', salary: kwota-pensji } - wartość minimalną zakresu,
- wartość maksymalną zakresu.
Zadaniem naszej funkcji ma być zwrócenie nowej tablicy, która będzie posiadać tylko te osoby, których dochód jest większy niż wartość minimalna i mniejszy niż maksymalna.
Poniżej przykład wywołania takiej funkcji.
const employees = [
{ name: 'Amanda Doe', salary: 3000 },
{ name: 'John Doe', salary: 4000 },
{ name: 'Claire Downson', salary: 2000 },
{ name: 'Freddie Clarkson', salary: 6000 },
{ name: 'Thomas Delaney', salary: 8200 }
];
const filteredEmployees = filterEmployees(employees, 2000, 8000);
console.log(filteredEmployees);
/* It should return
[{
{ name: 'Amanda Doe', salary: 3000 },
{ name: 'John Doe', salary: 4000 },
{ name: 'Freddie Clarkson', salary: 6000 },
}];*/
Pokaż odpowiedź Ukryj odpowiedź
function filterEmployees(arr, min, max) {
const filteredArray = [];
for(const empl of employees) {
if(empl.salary > min && empl.salary < max) {
filteredArray.push(empl);
}
}
return filteredArray;
}
const employees = [
{ name: 'Amanda Doe', salary: 3000 },
{ name: 'John Doe', salary: 4000 },
{ name: 'Claire Downson', salary: 2000 },
{ name: 'Freddie Clarkson', salary: 6000 },
{ name: 'Thomas Delaney', salary: 8200 }
];
const filteredEmployees = filterEmployees(employees, 2000, 8000);
console.log(filteredEmployees);
Ćwiczenie 5
Napisz funkcję, która przyjmie jeden argument (dowolny obiekt), a następnie wyświetli w konsoli informacje o jego wszystkich właściwościach.
Np. dla obiektu:
const obj = {
firstName: 'John',
lastName: 'Doe'
}
Wyświetli:
firstName: John
lastName: Doe
Pokaż odpowiedź Ukryj odpowiedź
function showObjectParams(obj) {
for(const paramId in obj) {
const param = obj[paramId];
console.log(paramId + ': ' + param);
}
}
Ćwiczenie 6
Napisz funkcję forEach, która przyjmie dwa argumenty:
- dowolną tablicę,
- dowolną funkcję callback.
Zadaniem funkcji powinna być przejście po każdym elemencie tablicy i wywołanie dla każdego z osobna funkcji callback. Tej, którą otrzymamy w drugim parametrze. Co ważne, ta funkcja callback powinna być wywołana z jednym argumentem, równym właśnie obsługiwanemu w danej chwili elementowi.
Możesz wspomóc się powrotem do pierwszego modułu z projektem pizzerii. Tam bardzo dużo czasu spędziliśmy nad omawianiem wykorzystania funkcji callback. Przykłady tam dostępne mogą Ci dużo rozjaśnić.
Np. takie wywołanie:
forEach(['John', 'Amanda', 'Thomas'], function(item) { console.log(item); });
...powinno dać nam w konsoli następujący rezultat:
John
Amanda
Thomas
Pokaż odpowiedź Ukryj odpowiedź
function forEach(arr, cb) {
for(const elem of arr) {
cb(elem);
}
}
Ćwiczenie 7
Napisz funkcję formatName, która przyjmie w argumencie imię i nazwisko, a następnie zwróci poprawioną jego formę. Poprawioną, czyli taką, w której tylko pierwsza litera imienia i nazwiska będą duże, a pozostałe małe.
formatName('aMAnDa dOE'); // returns Amanda Doe
formatName('AMANDA DOE'); // returns Amanda Doe
formatName('john DOE'); // returns John Doe
Bardzo pomocne mogą być wbudowane w JS funkcje:
split– tę już znasz.charAt– zwraca informacje o znaku obecnym na danej pozycji w stringu.substr– pozwala na zwrócenie tylko części stringu (np. wszystkich liter z wyjątkiem pierwszej).toUpperCase– pozwala na zmianę liter na duże (np.aadoAA).toLowerCase– analogicznie jak wyżej, tylko w drugą stronę.
Uwaga! Załóż, że bierzemy pod uwagę tylko proste tożsamości. Nie musisz obawiać się, że funkcja otrzyma tylko imię albo dwuczłonowe nazwisko.
Pokaż wskazówkę Ukryj wskazówkę
const str = 'Jeremy';
const newStr = str.charAt(0).toLowerCase() + str.substr(1).toUpperCase();
console.log(newStr); // returns jEREMY
Pokaż odpowiedź Ukryj odpowiedź
function formatName(name) {
const firstNameAndLastName = name.split(' ');
let firstName = firstNameAndLastName[0];
let lastName = firstNameAndLastName[1];
firstName = firstName.charAt(0).toUpperCase() + firstName.substr(1).toLowerCase()
lastName = lastName.charAt(0).toUpperCase() + lastName.substr(1).toLowerCase()
return firstName + ' ' + lastName;
}
console.log(formatName('AMANdA doE')); // returns Amanda Doe
Ćwiczenie 8
Przygotuj funkcję getEvensInRange, która przyjmie dwa argumenty:
- liczbę wskazującą początek zakresu do sprawdzenia,
- liczbę wskazującą jego koniec.
Zadaniem funkcji jest przejście po wszystkich liczbach wewnątrz podanego zakresu i zwrócenie tablicy, która będzie zawierać tylko te, które są parzyste.
Do wykonania zadania pomocne będzie przypomnienie sobie składni podstawowej pętli for oraz dzielenia modulo.
Przykład użycia funkcji:
getEvensInRange(0, 9); // returns [0, 2, 4, 6, 8]
getEvensInRange(7, 12); // returns [8, 10, 12]
Pokaż wskazówkę Ukryj wskazówkę
Aby sprawdzić, czy liczba jest parzysta, wystarczy podzielić ją przez dwa i ustalić, czy otrzymano zero reszty.
Pokaż odpowiedź Ukryj odpowiedź
function getEvensInRange(start, end) {
const evensArray = [];
for(let i = start; i <= end; i++) {
if(i%2 === 0) evensArray.push(i);
}
return evensArray;
}
Ćwiczenie 9
Czas na ostatnią funkcję w niniejszym submodule. Przypuszczalnie najtrudniejszą. Chociaż, kto wie... może akurat łatwo sobie z nią poradzisz?
Twoim zadaniem jest napisanie funkcji o nazwie filter, która przyjmie dwa argumenty – dowolną tablicę oraz funkcję callback. Celem funkcji jest zwrócenie nowej przefiltrowanej tablicy, w której znajdą się tylko te elementy, dla których przekazana funkcja callback zwróci true.
Pomysł jest bardzo ciekawy. Dzięki takiej funkcji, bylibyśmy w stanie bardzo szybko i łatwo dowolnie przefiltrowywać wybraną tablicę. Wystarczy podać dane wejściowe i warunek do sprawdzenia (schowany w funkcji callback), a ta zwróci nam nowe dane. Już przefiltrowane!
Może brzmieć to dziwne, więc wspomożemy się małym przykładem.
filter([5, 6, 7], function(item) { return item%2 === 0 });
Przykładowo, odpalając filter jak wyżej, oczekiwalibyśmy, że jako wynik otrzymamy tablicę [6]. Dlaczego?
Jak mówiliśmy już wcześniej, filter powinno przejść po wszystkich elementach, dla każdego odpalić funkcję callback (przekazując jako argument obsługiwany w danej chwili element) i zależnie od tego, czy jej wywołanie zwróci true albo false, decydować o pozostawieniu go w nowej przefiltrowanej tablicy.
No więc pomyślmy. Najpierw filter będzie chciało sprawdzić pierwszy element tablicy, czyli 5. Uruchomi wiec funkcję callback function(item) { return item%2 === 0 }, przekazując jako pierwszy argument (item) właśnie wartość 5. Co taka funkcja zwróci? Dla 5 – false. Bo w końcu reszta z dzielenia 5 przez dwa na pewno będzie większa. Skoro więc funkcja callback zwróci nam false, to dany element nie będzie dodawany do nowej tablicy.
Następnie filter zajmie się kolejnym elementem 6. Wykona ten sam algorytm. Uruchomi funkcję callback, przekazując jako item 6. Co tym razem zwróci ta funkcja? true. Tym razem więc weźmiemy ten element pod uwagę, przy konstrukcji nowej przefiltrowanej tablicy.
Na końcu filter zajmie się 7. Tutaj znowu callback zwróci false, więc element nie będzie dodany do nowej przefiltrowanej tablicy.
Pokaż wskazówkę Ukryj wskazówkę
Jeśli nie masz pomysłu jak zacząć, to poniżej przedstawiamy nasz.
- Zacznij od utworzenia nowej pustej tablicy. To właśnie do niej będziemy zbierać za moment "pasujące" elementy.
- Przygotuj pętle
for, która przejdzie po każdym elemencie tablicy przekazanej w formie argumentu. - Wewnątrz tej pętli przygotuj kod, który uruchomi funkcję callback, przekazując jej jako argument aktualnie obsługiwany przez pętlę element. Następnie sprawdź, czy jej wywołanie zwróciło
trueczyfalse. Jeślitrue, to dodaj ten element do naszej nowej tablicy. Do tej, którą utworzyliśmy w punkcie pierwszym. - Po pętli dodaj instrukcję, która zadba o zwrócenie naszej przefiltrowanej tablicy.
Pokaż rozwiązanie Ukryj rozwiązanie
function filter(arr, cb) {
const filteredArray = [];
for(const elem of arr) {
if(cb(elem)) filteredArray.push(elem);
}
return filteredArray;
}
Podsumowanie
Na sam koniec, musimy kilka razy uderzyć się w pierś. Powiedzieliśmy, że to tylko rozgrzewka, a jednak niektóre zadania mogły dać Ci w kość. No cóż, nie ukrywamy, że taki był nasz cel. Zadania miały być stosunkowo krótkie, ale ich poziom miał być w założeniu minimalnie wyższy, niż ten, który pozwoliłby Ci na wykonanie ich bez zająknięcia. Chcieliśmy dać Ci szanse na ich wykonanie, ale jednak zmusić do naprawdę wytężonej pracy. I jak? Chyba nam się to udało? ;)
Dlaczego o tym mówimy? Dlatego, że nie chcemy, aby to jak Ci z nimi poszło, w jakiś sposób Cię martwiło. Tak naprawdę tego typu zadania mają to do siebie, że im więcej ich robimy, tym lepiej nam idzie. A jeśli nie robimy ich wcale, to na początku bardzo się męczymy. Zauważ, że w gruncie rzeczy, wszystkie były w Twoim zasięgu. Większość metod, pomysłów, które należało użyć, były już obecne w kursie, ale... często wykorzystywaliśmy je w prostszych i bardziej "logicznych" przykładach. Tym razem, chcieliśmy pokazać JS-a trochę z innej strony i uzmysłowić Ci, że to często również w pewien sposób praca, w której na co dzień rozwiązujemy "zagadki", a wtedy bez logicznego i analitycznego myślenia, ciężko sobie z nimi radzić. Jeśli jednak nie poszło Ci dobrze, nie martw się. Obie te umiejętności można zdecydowanie poprawić właśnie przez praktykę.
Pamiętaj też, że liczba popularnych problemów, na które możesz natrafić podczas kodowania, nie jest nieskończona. Zauważ, że nawet w naszych przykładach, mimo tego, że każdy traktował o czymś innym, to często rozwiązania były bardzo podobne. Wykorzystywały te same pomysły.
10.2. Manipulacja DOM-em i powtórka z OOP
W poprzednim module zajmowaliśmy się głównie tym, czego użytkownik nie widzi. Jeśli jakieś informacje wyświetlaliśmy, to tylko w konsoli. Tym razem bardzo mocno skupimy się za to na DOM-ie. Oczywiście, wciąż będziemy korzystać z pętli for czy działać na zbiorach danych, nadal pojawią się funkcje, ale tym razem dojdzie jednak do tego dużo działań na elementach DOM. Ponownie skorzystamy również z biblioteki Handlebars.
Wszystkie zadania z tego submodułu będą tyczyły się stosunkowo małych funkcjonalności. Będą one jednak częścią trochę większej aplikacji. Tym samym z każdym zadaniem będziemy rozwijać jeden i ten sam projekt.
Nie będzie to nic wielkiego. Poziom skomplikowania na pewno nie zbliży się nawet o krok do Pizzerii. To, co zbudujemy będzie tylko prostą aplikacją z listą książek. Naszym zadaniem będzie tylko generowanie odpowiednich tytułów na stronie, możliwość dodawania ich do listy "ulubionych" oraz zaoferowanie prostego filtrowania. Nic wielkiego, prawda?
Zanim zabierzemy się do pracy, zacznij od pobrania plików startowych:
Zadbaj również o pobranie odpowiednich paczek:
npm install
Struktura projektu może wydawać Ci się bardzo znajoma. Nie bez powodu. Aby ułatwić Ci zadanie, oparliśmy strukturę plików i katalogów na tym, co znasz już z Pizzerii. Oczywiście, tym razem będziemy pracować nad mniejszą aplikacją, samych plików będzie więc znacznie mniej, tak samo, jak funkcji pomocniczych czy arkuszy ze stylami, ale to chyba powinno tylko ułatwić sprawę, prawda?
Podsumujmy więc, co tu mamy.
W pliku index.html posiadamy bardzo prostą strukturę:
<main>
<div class="container">
<!-- FILTERS FORM -->
<section class="filters">
<form>
<label>
Adults only: <input name="filter" value="adults" type="checkbox">
</label>
<label>
Non-fiction: <input name="filter" value="nonFiction" type="checkbox">
</label>
</form>
</section>
<!-- BOOKS PANEL -->
<section class="books-panel">
<h1>Your collection</h1>
<ul class="books-list">
</ul>
</section>
</div>
</main>
<!-- BOOK TEMPLATE -->
<script id="template-book" type="text/x-handlebars-template">
<li class="book">
<!-- title and price -->
<header class="book__header">
<h2 class="book__name">{{ name }}</h2>
<p class="product__base-price no-spacing">${{ price }}</p>
</header>
<!-- cover image -->
<a href="#" class="book__image" data-id="{{ id }}">
<figure>
<img src="{{ image }}" alt="{{ name }}">
</figure>
</a>
<!-- ratings -->
<div class="book__rating">
<div class="book__rating__fill">
{{ rating }}/10
</div>
</div>
</li>
</script>
Jest tu formularz do filtrowania książek, ich lista (na razie) pusta i szablon, na którego podstawie będziemy te książki renderować.
Stylami nie będziemy się zajmować. Jest ich bardzo mało, a jeśli coś będzie potrzebne w celu wykonania któregoś z zadań, to na pewno Ci o tym powiemy. Na razie więc się nimi kompletnie nie przejmuj.
Ważniejsze jest to, co w plikach JS. Ponownie mamy aż trzy pliki:
- data.js
- functions.js
- script.js
W pierwszym (podobnie jak na początku w pizzerii) jest po prostu obiekt z danymi. Tym razem jednak zamiast produktów mamy naturalnie książki.
const dataSource = {}; // eslint-disable-line no-unused-vars
dataSource.books = [
{
id: 1,
name: 'Lady in red',
price: 20,
rating: 8,
image: 'images/books/1.jpg',
details: {
adults: true,
nonFiction: false
}
},
{
id: 2,
name: 'Eloquent Javascript',
price: 15,
rating: 9.2,
image: 'images/books/2.jpg',
details: {
adults: false,
nonFiction: true
}
},
...
W drugim (functions.js) znajdzie się tym razem tylko jedna funkcja pomocnicza – utils.createDOMFromHTML. Pamiętasz, do czego służy, prawda? Pomoże nam w generowaniu elementów DOM na podstawie kodu HTML. Używaliśmy tej funkcji nagminnie w projekcie pizzerii.
Plik script.js jest na razie pusty. To właśnie tu znajdzie się cały Twój kod.
No i jak? Wszystko jasne? Jeśli tak, to bierzmy się do pracy!
Ćwiczenie 1
Zaczniemy od czegoś stosunkowo prostego.
Twoim zadaniem jest utworzenie i wywołanie funkcji, która przejdzie po wszystkich książkach z dataSource.books i wyrenderuje dla nich reprezentacje HTML w liście .books-list. Oczywiście musisz wykorzystać w tym celu dostarczony już szablon (#template-book).
Staraj się wspierać kodem, który napisaliśmy już razem w pizzerii. Zobacz, jak tam radziliśmy sobie z taką funkcjonalnością. Przejrzyj np. klasę Product czy też Cart. Oczywiście tutaj nie musisz tworzyć żadnej klasy, chodzi o prostą funkcję. Nie będzie tu niczego nowego. Dasz sobie radę?
Pokaż wskazówkę Ukryj wskazówkę
Jeśli masz problem, aby zacząć, to poniżej przedstawiamy algorytm, który powinien znacząco Ci pomóc:
- Przygotuj referencję do szablonu oraz listy
.books-list. - Dodaj nową funkcję
render. - Wewnątrz niej przejdź po każdym elemencie z
dataSource.books. Pamiętaj, że plikscript.jsma do tego obiektu bezpośredni dostęp. - Wewnątrz tej pętli zadbaj o wygenerowanie kodu HTML na podstawie szablonu oraz danych o konkretnej książce.
- Na postawie tego kodu HTML wygeneruj element DOM.
- Wygenerowany element DOM dołącz jako nowe dziecko DOM do listy
.books-list.
Jeśli wszystko poszło dobrze, to strona na tym etapie powinna wyglądać następująco:
Tym razem nie pokażemy Ci już rozwiązania. Nie chcemy kusić Cię gotowcem, kiedy wiemy, że spokojnie dasz sobie radę :)
Ćwiczenie 2
Kolejną funkcjonalnością będzie możliwość dodawania książek do ulubionych.
Od strony użytkownika sam proces dodania powinien opierać się na dwukrotnym kliknięciu na obrazek książki. Klikasz dwa razy na okładkę książki i zostaje ona dodana do ulubionych. Powinno być to równoznaczne dodaniu do .book__image książki nowej klasy – favorite. Nie będziemy jej stylować, gdyż jest już gotowa. Wystarczy, że ją nadamy, a okładka automatycznie zmieni swój styl.
Od strony technicznej powinno wyglądać to tak, że w JS będziemy posiadać specjalną tablicę – favoriteBooks. Będzie to tablica z identyfikatorami książek, które dodano do ulubionych. Kliknięcie dwukrotnie okładkę książki to dodanie jej id do tablicy. Dzięki temu w każdej chwili będziemy w stanie kontrolować, jak wygląda sytuacja z naszymi książkami, bez potrzeby sprawdzania HTML-a.
Powinno to działać następująco:
Podsumowując, musisz dodać do JS-a pustą tablicę favoriteBooks oraz odpowiedni kod, który zadba o to, aby przy dwukliku na okładkę dowolnej książki, jej id było dodawane do tej tablicy, a do samego elementu HTML została dodana klasa favorite. Tyle.
Dodatkowo mamy dla Ciebie trzy informacje:
- Choć nie jest to najwydajniejsza opcja, możesz skorzystać z pętli
for, która nada nasłuchiwacz dla każdego elementu z osobna. - Event dwukliku nazywa się
dblclick. - Każdy link
.book__imageposiada atrybutdata-id. Może Ci się on bardzo przydać.
Cały kod odpowiedzialny za dodanie nasłuchiwaczy powinien być zamknięty w nowej funkcji initActions. Nie jest to konieczne, ale dobrze starać się korzystać ze znanych już praktyk. Stosowanie znanych już nazw funkcji ułatwi nam nawigację w projekcie.
Zadbaj o to, aby funkcja ta była uruchamiana po funkcji render.
Spróbuj wykonać to zadanie bez wskazówek. Jeśli jednak przez długi czas nie będziesz mieć rezultatów, to poniżej, tak jak w poprzednim zadaniu, przedstawiamy gotowy plan algorytmu.
Pokaż wskazówkę Ukryj wskazówkę
- Zacznij od dodania nowej pustej tablicy
favoriteBooks. - Dodaj funkcję
initActions. - Przygotuj w niej referencję do listy wszystkich elementów
.book__imagew liście.booksList. - Następnie przejdź po każdym elemencie z tej listy.
- Dla każdego z nich dodaj nasłuchiwacz, który po wykryciu uruchomi funkcję, która...
- ...zatrzyma domyślne zachowanie przeglądarki (
preventDefault), - doda do klikniętego elementu klasę
favorite, - pobierze z jego
data-ididentyfikator książki, - i doda ten identyfikator do
favoriteBooks.
Ćwiczenie 3
Nasz funkcjonalność "lajkowania" książek działa już całkiem ładnie, ale niestety tylko w jedną stronę... A co w sytuacji, gdybyśmy jednak chcieli "odlajkować" dany tytuł?
Właśnie to będzie tematem tego ćwiczenia.
Musisz tak zmodyfikować swoją funkcję przypiętą do nasłuchiwacza, aby najpierw sprawdzała, czy książka nie jest już w "ulubionych". Jeśli nie jest, to funkcja ma działać tak, jak dotychczas, a więc dodać klasę favorite i zapisać id książki w tablicy favoriteBooks. Jeśli jednak jest, to skrypt powinien usuwać id takiej książki z tablicy favoriteBooks oraz zabierać takiemu elementowi klasę favorite.
Efekt powinien być następujący:
Pokaż wskazówkę Ukryj wskazówkę
Tym razem nie będziemy wspomagać Cię algorytmem. Zadanie jest bowiem naprawdę krótkie. Wspomożemy Cię tylko w jednej kwestii, sprawdzenia, czy dana książka jest już w ulubionych, czy nie.
Masz dwie możliwości.
- Sprawdź, czy
favoriteBookszawiera id zapisanedata-idtej książki. Jeśli tak, to wiemy już, że jest w "ulubionych". - Sprawdź, czy element zawiera klasę
.favorite. Jeśli ma, to również oznacza to, że tak dana książka musi być już w "ulubionych".
Ćwiczenie 4
Już w ćwiczeniu drugim powiedzieliśmy, że idea dodawania osobnego nasłuchiwacza dla każdej książki to trochę słaby pomysł. Dlaczego?
Pomyśl tylko. Mamy na razie 5 książek i 5 nasłuchiwaczy. Czyli JS na bieżąco musi "nasłuchiwać" aż na 5 elementów. A gdyby tych książek było więcej, np. 100 albo 200? JS musiałby nasłuchiwać na 100 lub 200 elementów jednocześnie!
Na szczęście możemy coś na to zaradzić. W takich sytuacjach najczęściej stosuje się tzw. event delegation. Technika ta polega na tym, że zamiast nasłuchiwać na pojedyncze elementy, nasłuchuje się na cały kontener. Właśnie ten, w którym te elementy są przechowywane. W takiej sytuacji JS obserwuje tylko na jeden element, cały kontener. Dopiero kiedy wykryje, że dany event się wydarzył, określa się już konkretnie, na którym elemencie doszło do działania (np. kliknięcia).
W naszej sytuacji moglibyśmy więc nasłuchiwać zamiast na pojedyncze książki, na całą ich listę. Po wykryciu kliknięcia nasza funkcja po prostu ustalałaby, czy faktycznie kliknięto na okładkę, a nie np. na tytuł. Następnie wykonywałaby te same operacje, które dotychczas mieliśmy już zapisane dla pojedynczych nasłuchiwaczy.
To rozumowanie może mieć dla Ciebie jedną niewiadomą. Skąd funkcja callback będzie wiedzieć, co dokładnie kliknięto? Gdybyśmy taki dostęp do klikniętego elementu mieli, to problemu nie ma. Łatwo możemy sprawdzić, czy jego classList zawiera klasę book__image (classList.contains('book__image). Jeśli tak, to wiadomo, że to właśnie okładka książki. Tyle że aby to było możliwe, musimy mieć dostęp do elementu, który brał udział w zdarzeniu. Tego klikniętego. Tylko wtedy będziemy w stanie to sprawdzić.
Na szczęście mamy! Znasz już nasz obiekt event, prawda? Na razie ciągle mówiliśmy, że mamy w nim metodę preventDefault. Jest tam jednak znacznie więcej informacji. M.in. znajdziemy tam właściwość target, która... jest właśnie referencją do elementu, który brał udział w zdarzeniu! Czyli, w przypadku dblClick elementu, który został kliknięty!
No i jak, teraz powinno już być z górki, prawda?
Twoim zadaniem jest więc taka modyfikacja funkcji initActions, aby zamiast nadawać nasłuchwiacze na wszystkie okładki książek z osobna, dodawała tylko jeden na całą listę. Sama funkcja w środku nasłuchiwacza (callback) powinna być prawie identyczna. Z tym, że teraz dostęp do elementu klikniętego będzie możliwy tylko przez event.target, a zanim funkcja zacznie wykonywać dalsze operacje, powinna sprawdzić, czy event.target jest w ogóle checkboxem, a więc, czy posiada klasę .book__image (event.target.classList.contains('.book__image')).
Możesz wspomóc się również artykułem na blogu MDN. Wykorzystuje on inny sposób na sprawdzenie, czy kliknięty element jest tym, o który nam chodzi, ale również tłumaczy, o co chodzi w delegacji eventów.
Jeśli wszystko pójdzie dobrze, Twój kod powinien działać tak samo, jak wcześniej. Chociaż jak wiemy, pod maską, będzie to robić wydajniej.
Ćwiczenie 5
Została nam już tylko jedna ze wspomnianych funkcjonalność – filtrowanie książek przy użyciu formularza. Pomysł jest dość prosty. Każda książka posiada obiekt details, który określa, czy jest on fiction czy non-fiction, oraz czy nadaje się tylko dla dorosłych.
Spójrz chociażby na książkę "Lady in Red".
{
id: 1,
name: 'Lady in red',
price: 20,
rating: 8,
image: 'images/books/1.jpg',
details: {
adults: true,
nonFiction: false
}
},
Co chcemy zrobić? Jak ma to działać w praktyce? Chcemy, aby wybranie danego filtra odpowiednio modyfikowało naszą listę książek. Zaznaczamy adults only – w liście powinny znaleźć się tylko te książki, których details.adults równa się true. Zaznaczamy non-fiction – na liście powinny znaleźć się tylko te książki, których details.nonFiction równa się true. Zaznaczmy obie opcje, to analogicznie pojawią się tylko te książki, które mają w details obie właściwości ustawione na true.
Będzie to działało następująco:
Jak widzisz, książki nie będą "naprawdę" znikać. Po prostu będziemy je "wyszarzać" przy użyciu klasy.
Zadanie nie wydaje się zbyt skomplikowane, ale jednak będzie wymagało od nas trochę pracy. Dlatego też podzielimy je na dwa etapy.
Etap 1
Zacznij od dodania do kodu pustej tablicy filters. To w niej będziemy przechowywać informacje, jakie aktualnie filtry są wybrane w aplikacji.
Następnie przygotuj referencję do formularza w .filters. Dodaj też do initActions nowy nasłuchiwacz, który będzie obserwować właśnie nasz formularz i kiedy wykryje jakiekolwiek kliknięcie, to uruchomi funkcję callback.
W funkcji tej sprawdzaj, czy kliknięto na element, który faktycznie jest naszym checkboxem (czy jego tagName to INPUT, type to checkbox, a name to filter), a jeśli tak, to pokaż w konsoli, jego wartość (value).
Jak widzisz, ponownie wykorzystaliśmy właśnie koncept event delegation, brawo!
Na razie konsola powinna już pokazywać, co wybraliśmy:
Od tej chwili kliknięcie na checkbox będzie już pokazywało w konsoli, co wybraliśmy, ale nie aktualizuje jeszcze samej tablicy filters, a powinno. W końcu chcemy, aby przechowywała ona aktualne informacje na temat filtrów.
Twoim zadaniem jest dokończenie tego etapu. Musisz zadbać, aby funkcja callback sprawdziła, czy input jest zaznaczony (właściwość checked) czy nie. Jeśli tak, to trzeba dodać value takiego filtra do tablicy filters. Jeśli jest za to odznaczony, to musimy go z takiej tablicy usunąć.
Znowu pomocne będą metody push, indexOf i splice.
Pokaż wskazówkę Ukryj wskazówkę
Właściwość checked zwraca true, jeśli checkbox jest zaznaczony i false jeśli nie jest.
Na końcu tego etapu, filters powinno zawsze mieć poprawne informacje na temat wybranych filtrów.
Etap 2
JS już wie, co wybrano, ale w żaden sposób nie wpływa to na naszą listę. Musimy na pewno zadbać więc o to, aby każdorazowo przy zmianie filtru, JS odpowiednio ją modyfikował. Możemy np. renderować ją od nowa, ale już w zmniejszonej formie. Możemy pójść również w trochę innym kierunku i po prostu chować/zmieniać niepasujące do filtrów książki. I jak udało Ci się zauważyć już wcześniej, my wybierzemy właśnie tę drugą opcję.
W CSS-ie jest już przygotowana specjalna klasa dla .book-image o nazwie .hidden, która będzie powodowała "zszarzenie" okładki takiej książki. Wystarczy więc, że przygotujemy funkcję, która przejdzie po wszystkich książkach z dataSource.books i dla tych, które nie pasują do filtrów, doda klasę hidden. Z kolei dla tych, które pasują do filtrów, upewni się, że tej klasy nie mają.
Jeśli to dla Ciebie za mało, to poniżej znajdziesz dokładniejszy opis algorytmu. Nie odkrywaj go jednak od razu. Daj sobie szansę i spróbuj napisać tę funkcję bez naszej pomocy.
Pamiętaj również, aby dodać wywołanie tej funkcji na końcu funkcji callback, którą wcześniej "przypięliśmy" do formularza (w initActions).
Efekt końcowy powinien być następujący:
Pokaż wskazówkę Ukryj wskazówkę
Rozpiszmy to trochę dokładniej.
- Zacznij od stworzenia nowej funkcji
filterBooks. Zadbaj, aby była wywoływana każdorazowo przy zmianie checkboxa w formularzu. - Wewnątrz tej funkcji przygotuj pętle, która przejdzie po wszystkich elementach
dataSource.books. - Stwórz zmienną
shouldBeHidden, która jest domyślnie równafalse. To właśnie tę zmienną wykorzystamy na końcu, aby ustalić, czy trzeba dodać do naszej książkihidden. Na starcie zakładamy, że nie trzeba (stąd wartośćfalse), ale po przejściu przez filtr może się to potem zmienić. - Następnie, dalej w tej pętli
for, utwórz kolejną. Taką, która przejdzie po tablicyfiltersi ustali, czy dany filtr pasuje do informacji o danej książce. Jeśli sprawdzamy filtradults, to musimy np. ustalić, czydetails.adultsw obiekcie książki jesttrue. A dokładnie... - ...jeśli dana właściwość powinna być
true, a nie jest, to należy zmienićshouldBeHiddennatruei przerwać działanie pętli (słowo kluczowebreak). W końcu skoro już pierwszy filtr nie jest spełniony, to po co szukać dalej? I tak, trzeba taką książkę wyszarzyć. - Po pętli
forz filtrami dodaj pętle warunkową, która sprawdzi wartośćshouldBeHidden. Jeśli jest równatrue, to należy znaleźć element.book__imagedanej książki i nadać mu klasęhidden. Jeśli jest równafalse, to należy taką klasę zabrać.
Jeśli masz problem z punktem 5, to sprawdź drugą wskazówkę. Jeśli masz problem z punktem 6, to sprawdź trzecią wskazówkę.
Pokaż wskazówkę nr 2 Ukryj wskazówkę nr 2
Punkt 5 może wydawać się trudny, ale wcale nie będzie aż tak źle.
for(const filter of filters) {
if(!condition) {
shouldBeHidden = true;
break;
}
}
To, co wewnątrz warunku jest raczej proste, ale możesz mieć problem z ustaleniem warunku. Zauważ jednak, że tak naprawdę informacje, które są nam potrzebne, są już w obiekcie książki, a dokładnie w jej właściwości details. Wystarczy więc np. sprawdzić, czy details.adults danej książki równa się true albo, czy details.nonFiction równa się true.
Jedyna trudność to więc fakt, że nie możemy w warunku wpisać np. od razu:
if(!details.adults) {
...
}
Bo raz sprawdzamy adults, a raz nonFiction, ale... przecież nazwa właściwości do sprawdzenia jest równa wartości filter. Tym samym wystarczy:
if(!book.details[filter]) {
...
}
...które sprawdzi, czy dana właściwość, której nazwa to wartość filter jest false.
Pokaż wskazówkę nr 3 Ukryj wskazówkę nr 3
Jak znaleźć taki element? Masz do dyspozycji id danej książki. Wiesz też, że book__image zawsze posiada atrybut data-id o wartości równej właśnie id książki. Aby znaleźć więc book__image odpowiadające danej książce, wystarczy znaleźć element o tejże klasie i data-id równym właśnie id książki.
.book__image[data-id="id-of-the-book-here"]
Pamiętasz? Podobne selektory wykorzystywaliśmy już w aplikacji bloga.
Ćwiczenie 6
Idzie nam tak dobrze... Może zrobimy coś "ekstra"?
Spójrz na nasz "rating" pod książkami.
Nie wygląda to zbyt dobrze, prawda? Ten szary kolor nie daje nam żadnej informacji. Nie uważasz, że byłoby znacznie ładniej, gdyby pasek lepiej oddawał ocenę? Np. gdy ocena to 5, wtedy połowa paska byłaby żółta. A jeśli 8, to prawie cały pasek byłby zielony.
Zauważ, że sam element jest nawet odpowiednio zbudowany do takich operacji:
Mamy tutaj div w divie. Ten wewnętrzny może być więc wykorzystany właśnie jako nasz kolorowy pasek. Wystarczy zadbać o to, aby zależnie od wartości rating danej książki zależał jego kolor i rozmiar. Np. rating 5 powinien oznaczać, że kolorem paska będzie żółty gradient, a jego rozmiar to 50% rodzica itd.
Żeby jakoś to sobie uporządkować, ustalmy kilka progów, co do wyglądu tła (naszego paska).
Rating < 6
background: linear-gradient(to bottom, #fefcea 0%, #f1da36 100%);
Rating > 6 && <= 8
background: linear-gradient(to bottom, #b4df5b 0%,#b4df5b 100%);
Rating > 8 && <= 9
background: linear-gradient(to bottom, #299a0b 0%, #299a0b 100%);
Rating > 9
background: linear-gradient(to bottom, #ff0084 0%,#ff0084 100%);
Sam rozmiar paska powinien być zależy tylko od samej wartości ratingu. Rating 0 to width: 0%, rating 55% to width: 55%, rating 9.9 to width: 99% itd.
Całość powinna dać nam na końcu następujący efekt:
Jak się za to w ogóle zabrać? Pomysł, który pewnie przychodzi Ci do głowy, to przygotowanie klas, następnie nadawanie ich w JS, zależnie od wartości rating. Cóż, w aplikacji z blogiem, coś w tym stylu zrobiliśmy. Dokładnie w panelu z chmurą tagów. Weź jednak pod uwagę, że wtedy tych klas mieliśmy kilka: tag-size-1, tag-size-2, tag-size-3 i jeszcze tylko parę kolejnych. Tutaj musielibyśmy przygotować aż 100 klas... Po jednej dla każdego ratingu. Do tego większość z nich w naszej sytuacji nie byłaby nawet wykorzystana. Jaka jest bowiem szansa, że na naszej liście będzie akurat minimum 100 książek i do tego każda będzie miała inny rating?
Dlatego tym razem lepiej pomyśleć o innym sposobie nadaniu stylów. Jeszcze nigdy tego nie robiliśmy, ale musisz wiedzieć, że elementy można stylować nie tylko przy użyciu reguł CSS, ale również bezpośrednio w HTML.
Np.
<a style="color: red">
Przeważnie tego unikamy, gdyż właściwości zapisane w style mają wysoki priorytet, przez co ciężko, byłoby je w razie potrzeby nadpisać. W specjalnych sytuacji jednak możemy z nich skorzystać. Taki zapis daje nam bowiem bardzo ważną zaletę. Możemy bowiem traktować style jak każdy inny atrybut. Jako właściwość obiektu DOM, a tym samym bardzo łatwo również ją modyfikować. Np. ustalić, że elem.style.background powinno mieć wartość jakiegoś gradientu.
Oczywiście w taki sposób modyfikujemy tylko jeden konkretny element, ale przecież i tak będziemy musieli stylować każdy element indywidualnie. W końcu każdy ma inny rating. Nie będzie więc to dla nas problem.
Więcej o style możesz przeczytać tutaj.
Pozostaje jednak jeszcze inna kwestia. Wiemy jak nadać konkretne style, ale od czego zacząć? Zrobić jakąś funkcję?
Prawdopodobnie, pamiętając już jak rozwiązywaliśmy poprzednie zadanie, wydaje Ci się, że powinniśmy zacząć od napisania funkcji, która przejdzie po każdym elemencie w HTML i odpowiednio zmieni jego style, a konkretnie ustawi style.background. No i cóż... jest to opcja, która jak najbardziej jest możliwa do wykonania, ale warto się zastanowić, czy naprawdę nie da się tego zrobić łatwiej.
Zauważ, że wcześniej musieliśmy faktycznie modyfikować elementy na stronie, kiedy te były już wyrenderowane. Kiedy coś "lajkowaliśmy", książki były już na stronie. Trzeba było więc "odpowiednią" znaleźć i z modyfikować. Chcieliśmy włączyć filtry – podobnie. Mieliśmy więc dynamiczne operacje, które mogły być wykonane raz po 2 minutach, raz po 5 minutach, a innym razem np. w ogóle. Wszystko zależało od użytkownika.
Teraz jednak chcemy przygotować coś, co będzie stałe od samego początku. Przecież rating nigdy się nie zmieni. W takim razie spokojnie możemy zadbać o nadanie stylu już w momencie generowania widoku na podstawie szablonu.
Pomyśl tylko, jaki sens miałoby bowiem generowanie wszystkich książek bez kolorowego paska, tylko po to, aby za chwilę znowu po nich przejść i ten kolor nadać? Lepiej od razu przygotować w szablonie placeholder, który przyjmie ten styl i ustalać go przed tym, jak generujemy widok danej książki.
No i jak? Może spróbujesz zrobić to zadanie bez naszej pomocy? ;)
Jeśli mimo wszystko nie wiesz jak zacząć, to sprawdź wskazówki poniżej. Każda kolejna wskazówka to kolejny krok lub kroki. Postaraj się korzystać z nich, tylko jeśli już naprawdę będzie Ci brakować jakiekolwiek pomysłu.
Pokaż wskazówkę Ukryj wskazówkę
- Zacznij od modyfikacji szablonu
#template-book. Musisz dodać dobook__rating__fillatrybutstyle, którego wartość ma być równa"width: {{ ratingWidth }}%; background: {{ ratingBgc }}". Zauważ, że mamy tutaj dwa nowe placeholdery. Co to oznacza? Że posiadamy dwa nowe miejsca, których wartość będziemy mogli dynamicznie ustawić podczas generowania widoku. Raz np.ratingWidthbędzie równe20więc da namwidth: 20%, a innym 99 itd.
Pokaż wskazówkę nr 2 Ukryj wskazówkę nr 2
- Dodaj do aplikacji nową funkcję
determineRatingBgc. Powinna ona przyjmować jako argument rating i zwracać odpowiedni background. Zależnie od ratingu będzie tolinear-gradient(to bottom, #b4df5b 0%,#b4df5b 100%);albo inny z podanych wyżej backgroundów. Pamiętaj, że należy traktować te wartości jako zwykły string. - Odnajdź funkcję
renderi w pętli, która przechodzi po każdym produkcie, przygotuj stałąratingBgc, która będzie równa temu, co zwrócidetermineRatingBgcdlaratingdanej książki. - Przygotuj również stałą
ratingWidth, która ustali długość paska. Musi się ona opierać na wartości ratingu. Jeśliratingrówna się 5, toratingWidthmusi równać się 50 itd. Czyli tak naprawdę konwertujemyratingdo skali procentowej.
Teraz pozostało już tylko przekazać te informacje do szablonu, przy generowaniu widoku.
Zadanie: powtórka z OOP
Na koniec modułu czas na obiecaną powtórkę z OOP. Póki co, całą naszą aplikację budowaliśmy w bardzo prosty sposób, dodając po prostu kolejne funkcje. Im jednak ich więcej, tym mniej czytelny staje się kod. Pamiętasz, jak ciężko nawigowało się po aplikacji bloga, gdy ta się rozwinęła? Spójrz tylko na to, jak w tej chwili wygląda script.js. A gdyby doszły kolejne funkcjonalności, byłoby tylko gorzej... Wiesz już jednak, że w takich sytuacjach jest proste i znane Ci już wyjście – OOP. I właśnie konwersja naszej aplikacji na styl OOP będzie Twoim zadaniem.
Co mamy przez to na myśli? Przede wszystkim cała logika aplikacji będzie "schowana" w klasie BooksList, a wszystkie referencje do DOM-u, tablice, funkcje itd. powinny być częścią klasy – w formie właściwości (np. tablice) albo metod (funkcje). Poza ciałem klasy ma znaleźć się tylko jedna linijka kodu, która po prostu utworzy jedną instancję na niej opartą.
class BooksList {
...
}
const app = new BooksList();
W pracy nad klasą staraj trzymać się pomysłów, które wykorzystaliśmy już wcześniej w pizzerii. Np. referencje do elementów DOM warto przechowywać we właściwościach this, a za ich nadanie może odpowiadać metoda o nazwie getElements.
Jeśli przez dłuższy czas będziesz mieć problem z rozpoczęciem zadania, to poniżej przedstawiamy propozycję podziału klasy na metody. Użyj jej jednak tylko wtedy, jeśli naprawdę się zatniesz.
Pokaż wskazówkę Ukryj wskazówkę
class BooksList {
constructor() {
...
}
initData() {
this.data = dataSource.books;
}
getElements() {
...
}
initActions() {
...
}
filterBooks() {
...
}
determineRatingBgc() {
...
}
}
const app = new BooksList();
Gdy zadanie będzie już gotowe, opublikuj je na GitHubie, a link wyślij swojemu Mentorowi do sprawdzenia.
10.3. Podział JS na komponenty
Młodzi developerzy najczęściej uczą się na małych projektach. Takie podejście wydaje się mieć sens, ale mści się dosyć szybko, kiedy dołącza się do zespołu pracującego nad większym przedsięwzięciem. Właśnie dlatego po mniejszych zadaniach z wcześniejszych submodułów, teraz przyszedł czas na powrót do naszej pizzerii. Będziemy kontynuować pracę, dodając do niej kolejne funkcjonalności. Dzięki temu nauczymy się również, w jaki sposób możemy zastosować rozwiązania, które pomogą nam łatwo pracować z coraz większym projektem.
Ten i następny submoduł będzie mieć formę wideowarsztatów. Na nagraniach będziemy pokazywać i tłumaczyć kolejne kroki rozwoju projektu, a Twoim zadaniem będzie dodawanie tych funkcjonalności do swojej aplikacji. Nagrania będą przeplatane samodzielnymi zadaniami, które pomogą Ci utrwalić wiedzę.
Opis nowej funkcjonalności
Czym się zajmiemy? Na razie, możemy tylko zamawiać produkty. Teraz dodamy kolejną dużą funkcjonalność rezerwację stolików, która będzie znajdować się na zupełnie nowej podstronie.
Na stronie jest już podstrona zamawiania produktów. Będziemy więc musieli nie tylko przygotować nową, ale też zadbać o to, aby dało się pomiędzy nimi przełączać.
Wbrew pozorom, nie będzie to aż takie trudne. Obie podstrony będą zawarte w tym samym pliku index.html jako divy. Aby uzyskać przełączanie pomiędzy podstronami bez przeładowania strony, wystarczy odpowiednio chować jeden div, a pokazywać drugi. Tak naprawdę ten pomysł nie różni się wiele od mechanizmu przełączania artykułów, który zbudowaliśmy w aplikacji bloga. Tam też jeden artykuł się chował, drugi pokazywał.
Sama rezerwacja stolików będzie odbywać się poprzez wybór daty i godziny, dla której zostanie wyświetlony plan ustawienia stolików w restauracji. Każdy stolik będzie oznaczony jako dostępny lub zajęty. Dzięki temu klienci będą mogli bez problemu wybrać stolik, który chcą zarezerwować.
To może brzmieć jak bardzo rozbudowana funkcjonalność, ale za chwilę przekonasz się, że nie będzie to aż tak skomplikowane. Zanim jednak faktycznie się za to zabierzemy, zajmiemy się zmianą, która uporządkuje nam to, co w tym projekcie już mamy.
Zadanie: importowanie w pozostałych plikach
Dodaj importy w pozostałych plikach JS. Korzystaj z ESLinta jako wskazówki, które obiekty lub klasy jeszcze nie zostały zaimportowane. Czytaj komunikaty ESLinta – jeśli zaimportujesz obiekt, który nie jest używany w danym pliku, ESLint również zgłosi błąd.
Kiedy ESLint nie będzie już informował o żadnych błędach, uruchom npm run watch i sprawdź komunikaty wyświetlane w konsoli. Jeśli któraś ze ścieżek do importowanych plików jest nieprawidłowa, w konsoli zobaczysz błąd. Wtedy musisz sprawdzić ścieżki do importowanych plików. Pamiętaj, że ścieżka do importowanego pliku jest zawsze relatywna do pliku, w którym importujesz. W razie wątpliwości, wróć do przykładów które podaliśmy powyżej i wzoruj się na nich.
Efektem tych zmian powinna być strona działająca tak samo jak wcześniej – bez błędów zgłaszanych przez ESLinta czy konsolę.
Dzięki temu udało nam się zachować całą funkcjonalność, włącznie z dodawaniem do koszyka, ale z podziałem na kilka mniejszych plików z kodem JS. Będzie nam łatwiej się w nich odnaleźć, a nawet otwierać je obok siebie, jeśli mamy taką potrzebę.
Co więcej, przyzwyczajamy się w ten sposób do architektury projektu, która często jest stosowana w komercyjnych projektach, i z którą zapewne spotkasz się w swojej pracy. Przygotujesz się również do rozpoczęcia pracy z React.js, którym zajmiemy się już niedługo. Tam również spotkasz się z podejściem komponentowym.
10.4. Dodanie obsługi podstron
Za nami pierwszy etap usprawnienia naszej pracy w projekcie. Zanim przejdziemy do kolejnej zmiany, musimy go uzupełnić. Podobnie jak w poprzednim module, przygotowaliśmy dla Ciebie trochę kodu, abyśmy mogli się skupić na samych funkcjonalnościach.
Uzupełnienie kodu projektu
Tym razem, jednak, zamiast podawać Ci każdy plik z osobna i opisywać zmiany, umieściliśmy cały projekt w repozytorium. Znajdziesz w nim wszystkie niezbędne modyfikacje. Samodzielnie zdecyduj, czy wolisz podmienić pliki, w których zaszły zmiany, czy ręcznie wprowadzić zmiany do tych plików.
Oczywiście, z plików tego projektu usunęliśmy src/js/app.js oraz cały katalog src/js/components. Dzięki temu nie musisz się martwić, że nadpiszesz swój kod JS, który jest przedmiotem tego projektu. ;)
Zmiany w plikach projektu
Jak widzisz, zmieniliśmy plik src/index.html. W nagłówku dodaliśmy .main-nav, a pod nagłówkiem – wrapper #pages. W tym wrapperze znajdują się dwie sekcje podstron, których id odpowiadają linkom w .main-nav. Za chwilę wykorzystamy to powiązanie, aby dodać funkcjonalność przełączania się pomiędzy podstronami. Dzięki temu obie podstrony będą dostępne bez przeładowania strony.
Pozostałe zmiany to przede wszystkim: style, settings.js oraz utils.js. Uzupełniliśmy je o kod niezbędny do realizacji niniejszego modułu. W miarę jego realizacji zobaczysz, w jaki sposób je wykorzystujemy.
Dodaliśmy też kolejny obiekt w src/db/app.json, który zawiera informacje o wydarzeniach w naszej pizzerii, przez które niektóre stoliki będą zajęte. Zdecydowaliśmy się przechowywać je osobno od rezerwacji, ponieważ część z nich odbywa się cyklicznie. Będzie nam też wygodniej testować aplikację, kiedy rezerwacje zapisywane za pomocą naszej strony będą w osobnym obiekcie.
Pozostałe zmiany dotyczą stylów projektu – możesz śmiało dodać nowe pliki w src/sass/partials. W pozostałych plikach .scss jest niewiele zmian, więc bez problemu wprowadzisz je ręcznie, jeśli style Twojego projektu były przez Ciebie zmienione.
Dodajemy podstrony
Zadanie: stworzenie prostej klasy
Obsługa podstron już działa, ale nasza podstrona Booking jest cały czas pusta. Za chwilę to zmienimy!
Wbrew pozorom, nie będzie to aż takie trudne zadanie. Na razie skupimy się bowiem przede wszystkim na tym, by wygenerować tylko kod HTML na podstawie szablonu i uruchomić proste widgety liczbowe, a takie operacje robiliśmy już wiele razy. Chociażby w klasach Cart czy AmountWidget.
Zanim jednak zabierzesz się do pracy, spójrz, do czego dążymy:
Jak widzisz, naszym celem jest wygenerowanie dość złożonego widoku. Chcemy mieć input wyboru daty, godziny, planszę wyboru stolika i jeszcze kilka innych dodatkowych pól. Na szczęście odpowiedni szablon jest już gotowy. Twoim zadaniem będzie więc tylko wygenerowanie na jego podstawie kodu HTML i od tego zaczniemy. Dopiero później zadbamy jeszcze o uruchomienie inputów "Hours amount" i "people amount", a pozostałymi polami (np. wyborem godziny) zajmiemy się już w następnym module.
Etap 1
Ok, bierzmy się do pracy. Zaczniemy od wygenerowania widoku. Stwórz nową klasę o nazwie Booking. Zapisz ją oczywiście w formie nowego pliku w folderze components. Zadbaj też o jej eksport. Jej metodami zajmiemy się za chwilę.
Następnie przejdź do pliku App.js. Zaimportuj w nim klasę Booking oraz stwórz metodę app.initBooking, która będzie zajmować się inicjacją instancji tej klasy.
Zadbaj, aby metoda ta:
- znajdowała kontener widgetu do rezerwacji stron, którego selektor mamy zapisany w
select.containerOf.booking, - tworzyła nową instancję klasy
Bookingi przekazywała do konstruktora kontener, który przed chwilą znaleźliśmy, - była wywoływana na końcu metody
app.init.
Czyli od tego momentu, JS powinien na starcie aplikacji "z automatu" uruchamiać metodę app.initBooking, która utworzy nową instancję Booking, przekazując jej dostęp do całego kontenera na podstronie /booking. Musimy teraz zadbać, aby ta klasa faktycznie generowała cały widok, na razie bowiem nie robi nic. Jest pusta.
W samej klasie Booking przygotuj więc konstruktor, który:
- odbiera referencję do kontenera przekazaną w
app.initBooking, jako argument (np. o nazwieelement), - wywołuje metodę
render, przekazując tę referencję dalej (rendermusi mieć w końcu dostęp do kontenera), - wywołuje metodę
initWidgetsbez argumentów.
Następnie zajmij się metodą render.
Jej zadaniem jest:
- generowanie kodu HTML za pomocą szablonu
templates.bookingWidget, przy czym nie musimy przekazywać do niego żadnych danych, gdyż ten szablon nie oczekuje na żaden placeholder, - utworzenie pustego obiektu
thisBooking.dom, - dodanie do tego obiektu właściwości
wrapperi przypisanie do niej referencji do kontenera (jest dostępna w argumencie metody), - zmiana zawartości wrappera (
innerHTML) na kod HTML wygenerowany z szablonu.
Etap 2
Na końcu, czas zająć się naszymi inputami "Hours amount" i "People amount".
Musimy zacząć od przygotowania dostępu do obu inputów. W metodzie render dodaj wiec dwie nowe właściwości: dom.peopleAmount i dom.hoursAmount. Powinny być one referencjami odpowiednio do inputów "People amount" i "Hours amount". Selektory do nich znajdziesz w obiekcie select.booking. Żeby z niego korzystać, koniecznie go najpierw zaimportuj.
Następnie zajmij się metodą initWidgets. Zanim jednak do niej przejdziesz, zaimportuj do pliku klasę AmountWidget. Potem, już w samej metodzie, zadbaj o utworzenie nowych instancji AmountWidget na obu przygotowanych wcześniej elementach. Możesz wzorować się na metodzie CartPrduct.initAmountWidget. Pamiętaj, że teraz tworzymy dwa widgety, a nie jeden, i nie potrzebujemy jeszcze robić nic w momencie wykrycia zmiany. Nasłuchiwacze mogą więc już istnieć, ale ich funkcje callback nie muszą niczego przeliczać ani uruchamiać. Na dobrą sprawę, funkcje callback mogą być nawet na razie puste.
W efekcie tych zmian powinniśmy zobaczyć na podstronie Booking formularz z mapą restauracji, wygenerowany na podstawie szablonu, a klikanie guzików z plusem i minusem na inputach "People amount" i "Hours amount" powinno zmieniać wartości dla liczby osób oraz godzin.
Zauważ, że nasza klasa stosuje dokładnie te same pomysły, które znamy chociażby z klasy Cart. Wszystkie właściwości trzymamy w obiekcie this.dom. Widgety ilości tworzymy w initWidgets itd.
10.5. Podsumowanie
Ufff... To był bardzo pracowity tydzień...
To jeszcze nie koniec. W kolejnym module zajmiemy się dalszym rozwojem naszej aplikacji. Dokończymy podstronę rezerwacji. Pomyślimy też o dodatkowych funkcjonalnościach.
Czeka Cię jeszcze więcej praktyki i znacznie więcej debugowania, ale przy tym... rozwoju! W tym module jednak to już wszystko. Czas na zasłużony odpoczynek. Do zobaczenia!